package com.millstein.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.TypeReference;
import com.alipay.api.internal.util.AlipaySignature;
import com.baomidou.mybatisplus.core.conditions.query.Query;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.millstein.config.AlipayConfig;
import com.millstein.constant.OrderConstant;
import com.millstein.constant.RedisKey;
import com.millstein.context.UserAuthorization;
import com.millstein.enums.BizCodeEnum;
import com.millstein.enums.coupon.CouponStateEnum;
import com.millstein.enums.order.ClientTypeEnum;
import com.millstein.enums.order.PayTypeEnum;
import com.millstein.enums.order.ProductOrderStateEnum;
import com.millstein.enums.order.ProductOrderTypeEnum;
import com.millstein.exception.BizException;
import com.millstein.feign.CouponFeignService;
import com.millstein.feign.ProductFeignService;
import com.millstein.feign.UserFeignService;
import com.millstein.mapper.ProductOrderItemMapper;
import com.millstein.mapper.ProductOrderMapper;
import com.millstein.model.LoginUser;
import com.millstein.model.OrderMessage;
import com.millstein.model.ProductOrderDO;
import com.millstein.model.ProductOrderItemDO;
import com.millstein.properties.RabbitMQProperties;
import com.millstein.request.*;
import com.millstein.service.ProductOrderService;
import com.millstein.strategy.PayStrategyContext;
import com.millstein.util.CommonUtils;
import com.millstein.util.JsonData;
import com.millstein.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author millstein
 * @since 2021-09-17
 */
@Service
@Slf4j
@EnableConfigurationProperties(RabbitMQProperties.class)
public class ProductOrderServiceImpl extends ServiceImpl<ProductOrderMapper, ProductOrderDO> implements ProductOrderService {

    @Autowired
    private UserFeignService userFeignService;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private CouponFeignService couponFeignService;

    @Autowired
    private ProductOrderMapper productOrderMapper;

    @Autowired
    private ProductOrderItemMapper productOrderItemMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitMQProperties rabbitMQProperties;

    @Autowired
    private PayStrategyContext payStrategyContext;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 确认订单。步骤如下：
     *      1.生成订单前的操作
     *          * 防重提交
     *          * 用户微服务-确认收货地址
     *          * 商品微服务-获取最新购物项和价格
     *          * 订单验价
     *              * 优惠券微服务-获取优惠券
     *              * 验证价格
     *          * 锁定优惠券
     *          * 锁定商品库存
     *      2.创建订单
     *          * 创建订单对象
     *          * 创建子订单对象
     *          * 发送延迟消息-用于自动关单
     *          * 创建支付信息-对接三方支付
     *      3.生成订单后的操作
     *          * 成功时要根据客户端类型进行不同的处理
     *          * 失败要打印日志
     * @param addOrderRequest 订单信息
     */
    @Override
    @Transactional
    public void confirmOrder(AddOrderRequest addOrderRequest) {
        // 1.获取当前用户信息
        LoginUser loginUser = UserAuthorization.getUserContext();

        // 2.为了防止重复提交，需要检查一下参数中的token值
        this.checkSubmitToken(addOrderRequest.getToken(), loginUser.getId());

        // 3.生成订单编号
        String outTradeNo = RandomUtil.randomString(32);

        // 4.用户微服务-确认收货地址
        AddressVO addressVO = this.getAddressDetailFromRemote(addOrderRequest.getAddressId());

        // 5.商品微服务-获取最新购物项和价格
        CartVO cartVO = this.getProductDetailFromRemote(addOrderRequest.getProductIds());

        // 6.订单验价
        CouponRecordVO couponRecordVO = this.checkOrderAmount(cartVO, addOrderRequest);

        // 7.锁定优惠券
        this.lockCoupon(couponRecordVO, outTradeNo);

        // 8.锁定库存
        this.lockProductStock(cartVO, outTradeNo);

        // 9.创建订单对象
        ProductOrderDO productOrderDO = this.buildAndSaveProductOrder(loginUser, addOrderRequest, addressVO, outTradeNo);

        // 10.创建订单子对象
        List<ProductOrderItemDO> itemList = this.buildAndSaveProductOrderItem(cartVO, outTradeNo, productOrderDO.getId());

        // 11.发送延迟消息来关单
        this.sendDelayMessageToCloseOrder(outTradeNo);

        // 12.支付
        String form = this.doPlaceOrder(addOrderRequest, outTradeNo, itemList.get(0), null);

        // 13.按照结果、客户端类型和支付类型进行输出
        this.doOutputResultByType(form, addOrderRequest.getPayType(), addOrderRequest.getClientType());
    }

    /**
     * 防止重复提交
     * @param token 提交订单的token
     * @param id 用户id
     */
    private void checkSubmitToken(String token, Long id) {
        if (StrUtil.isBlank(token)) {
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_NOT_EXIST);
        }

        String redisKey = String.format(RedisKey.ORDER_SUBMIT_TOKEN, id);

        // 重复提交时会出现并发问题，使用lua脚本就能解决
        // 这个脚本的意思就是：
        // 如果get参数中第一个键的值和参数中第一个值相同，那么就删除参数中第一个键对应的键值对并返回其结果（成功时返回1）
        // 否则返回0
        String luaScript =
                "if redis.call('get', KEYS[1]) == ARGV[1] " +
                "   then return redis.call('del', KEYS[1]) " +
                "else return 0 end";

        // 执行lua脚本
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
                Collections.singletonList(redisKey),
                token);

        if (result.intValue() == 0) {
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
        }
    }

    /**
     * 用户微服务-确认收货地址
     * @param addressId 地址id
     * @return
     */
    private AddressVO getAddressDetailFromRemote(Long addressId) {
        JsonData result = userFeignService.findById(addressId);

        if (result.getCode() != 0) {
            throw new BizException(BizCodeEnum.ADDRESS_NO_EXITS);
        }
        return result.getData(new TypeReference<AddressVO>() {});
    }

    /**
     * 商品微服务-获取最新购物项和价格
     * @param productIds 商品id的集合
     * @return
     */
    private CartVO getProductDetailFromRemote(List<Long> productIds) {
        if (CollectionUtil.isEmpty(productIds)) {
            throw new BizException(BizCodeEnum.PRODUCT_IS_NOT_EXITS);
        }
        JsonData result = productFeignService.getProductFromCart(productIds);

        if (result.getCode() != 0) {
            throw new BizException(BizCodeEnum.ADDRESS_NO_EXITS);
        }
        return result.getData(new TypeReference<CartVO>() {});
    }

    /**
     * 订单验价
     * @param cartVO 本次下单的商品信息
     * @param addOrderRequest 订单信息
     */
    private CouponRecordVO checkOrderAmount(CartVO cartVO, AddOrderRequest addOrderRequest) {
        // 1.获取优惠券记录的信息
        CouponRecordVO couponRecordVO = this.getCouponRecordDetailFromRemote(addOrderRequest.getCouponRecordId());

        // 2.验价
        return doCheckOrderAmount(cartVO, addOrderRequest, couponRecordVO);
    }

    /**
     * 获取优惠券记录信息
     * @param couponRecordId 优惠券记录id
     * @return
     */
    private CouponRecordVO getCouponRecordDetailFromRemote(Long couponRecordId) {
        // 不使用优惠券的情况
        if (couponRecordId == null || couponRecordId < 1L) {
            return null;
        }
        JsonData result = couponFeignService.findCouponRecord(couponRecordId);
        if (result.getCode() != 0) {
            throw new BizException(BizCodeEnum.COUPON_RECORD_NO_EXITS);
        }
        return result.getData(new TypeReference<CouponRecordVO>() {});
    }

    /**
     * 订单验价
     * @param cartVO 实际的商品信息
     * @param addOrderRequest 订单信息
     * @param couponRecordVO 所用优惠券信息
     */
    private CouponRecordVO doCheckOrderAmount(CartVO cartVO, AddOrderRequest addOrderRequest, CouponRecordVO couponRecordVO) {
        // 1.计算商品总价
        BigDecimal totalAmount = BigDecimal.ZERO;

        for (CartItemVO item : cartVO.getCartItemList()) {
            totalAmount = totalAmount.add(item.getTotalAmount());
        }

        // 2.检查优惠券能不能用
        checkCouponUsable(couponRecordVO, totalAmount);

        // 3.计算实际的价格
        BigDecimal couponDeduceAmount = couponRecordVO == null ? BigDecimal.ZERO : couponRecordVO.getPrice();
        BigDecimal realPayAmount = totalAmount.subtract(couponDeduceAmount);
        // 防止扣减优惠券后实际支付金额小于0
        if (realPayAmount.compareTo(BigDecimal.ZERO) < 0) {
            realPayAmount = BigDecimal.ZERO;
        }

        // 4.验价（实际金额和总价要和计算出来的都一样，才认为是验证成功）
        if (totalAmount.compareTo(addOrderRequest.getTotalAmount()) != 0
                || realPayAmount.compareTo(addOrderRequest.getActualAmount()) != 0) {
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_PRICE_FAIL);
        }

        return couponRecordVO;
    }

    /**
     * 检查优惠券能不能用
     * @param couponRecordVO 优惠券记录信息
     * @param totalAmount 总价
     */
    private void checkCouponUsable(CouponRecordVO couponRecordVO, BigDecimal totalAmount) {
        // 判断优惠券状态是否为未使用
        if (!couponRecordVO.getUseState().equals(CouponStateEnum.NEW.name())) {
            throw new BizException(BizCodeEnum.COUPON_STATE_ERROR);
        }
        // 判断优惠券的使用条件金额有没有超过订单总额
        if (totalAmount.compareTo(couponRecordVO.getConditionPrice()) < 0) {
            throw new BizException(BizCodeEnum.COUPON_CONDITION_ERROR);
        }
        // 判断优惠券是否在使用时间内
        Date now = new Date();
        if (couponRecordVO.getStartTime().after(now) || couponRecordVO.getEndTime().before(now)) {
            throw new BizException(BizCodeEnum.COUPON_OUT_OF_TIME);
        }
    }

    /**
     * 锁定优惠券
     * @param couponRecordVO 优惠券记录信息
     * @param outTradeNo 订单号
     */
    private void lockCoupon(CouponRecordVO couponRecordVO, String outTradeNo) {
        LockCouponRequest lockCouponRequest = new LockCouponRequest();
        lockCouponRequest.setOutTradeOrderNo(outTradeNo);
        lockCouponRequest.setLockedCouponRecordIds(Collections.singletonList(couponRecordVO.getId()));

        JsonData result = couponFeignService.lockCoupon(lockCouponRequest);
        if (result.getCode() != 0) {
            log.error("锁定优惠券失败，{}", couponRecordVO);
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_LOCK_PRODUCT_FAIL);
        }
    }

    /**
     * 锁定库存
     * @param cartVO 订单中商品的信息
     * @param outTradeNo 订单号
     */
    private void lockProductStock(CartVO cartVO, String outTradeNo) {
        LockProductRequest lockProductRequest = new LockProductRequest();
        lockProductRequest.setOutTradeOrderNo(outTradeNo);
        lockProductRequest.setProducts(cartVO.getCartItemList().stream().map(item -> {
            LockProductItemRequest itemRequest = new LockProductItemRequest();
            itemRequest.setBuyNum(item.getBuyNum());
            itemRequest.setProductId(item.getId());
            return itemRequest;
        }).collect(Collectors.toList()));

        JsonData result = productFeignService.lockProduct(lockProductRequest);
        if (result.getCode() != 0) {
            log.error("锁定商品库存失败，{}", cartVO);
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_LOCK_PRODUCT_FAIL);
        }
    }


    /**
     * 构建并保存订单对象
     * @param loginUser 用户信息
     * @param addOrderRequest 订单信息
     * @param addressVO 地址信息
     * @param outTradeNo 订单号
     * @return
     */
    private ProductOrderDO buildAndSaveProductOrder(LoginUser loginUser,
                                                    AddOrderRequest addOrderRequest,
                                                    AddressVO addressVO,
                                                    String outTradeNo) {
        ProductOrderDO productOrderDO = new ProductOrderDO();
        productOrderDO.setCreateTime(new Date());
        productOrderDO.setDel(0);
        productOrderDO.setHeadImg(loginUser.getHeadImg());
        productOrderDO.setNickname(loginUser.getName());
        productOrderDO.setOrderType(ProductOrderTypeEnum.DAILY.name());
        productOrderDO.setOutTradeNo(outTradeNo);
        productOrderDO.setPayAmount(addOrderRequest.getActualAmount());
        productOrderDO.setPayType(addOrderRequest.getPayType());
        productOrderDO.setReceiverAddress(addressVO.toString());
        productOrderDO.setState(ProductOrderStateEnum.NEW.name());
        productOrderDO.setTotalAmount(addOrderRequest.getTotalAmount());
        productOrderDO.setUpdateTime(new Date());
        productOrderDO.setUserId(loginUser.getId());

        int rows = productOrderMapper.insert(productOrderDO);
        if (rows != 1) {
            throw new BizException(BizCodeEnum.SAVE_FAILURE);
        }
        return productOrderDO;
    }

    /**
     * 构建并保存订单子项
     * @param cartVO 商品信息
     * @param outTradeNo 订单编号
     * @param orderId 订单id
     * @return
     */
    private List<ProductOrderItemDO> buildAndSaveProductOrderItem(CartVO cartVO, String outTradeNo, Long orderId) {
        List<ProductOrderItemDO> itemList = cartVO.getCartItemList().stream().map(item -> {
            ProductOrderItemDO productOrderItemDO = new ProductOrderItemDO();
            productOrderItemDO.setProductOrderId(orderId);
            productOrderItemDO.setAmount(item.getPrice());
            productOrderItemDO.setBuyNum(item.getBuyNum());
            productOrderItemDO.setCreateTime(new Date());
            productOrderItemDO.setOutTradeNo(outTradeNo);
            productOrderItemDO.setProductId(item.getId());
            productOrderItemDO.setProductImg(item.getProductImage());
            productOrderItemDO.setProductName(item.getProductTitle());
            productOrderItemDO.setTotalAmount(item.getTotalAmount());
            return productOrderItemDO;
        }).collect(Collectors.toList());

        int rows = productOrderItemMapper.batchInsert(itemList);
        if (rows != itemList.size()) {
            throw new BizException(BizCodeEnum.SAVE_FAILURE);
        }
        return itemList;
    }

    /**
     * 发送延迟消息来关单
     * @param outTradeNo 订单编号
     */
    private void sendDelayMessageToCloseOrder(String outTradeNo) {
        OrderMessage orderMessage = new OrderMessage();
        orderMessage.setOutTradeNo(outTradeNo);
        rabbitTemplate.convertAndSend(rabbitMQProperties.getOrderEventExchange(),
                rabbitMQProperties.getOrderCloseDelayRoutingKey(), orderMessage);
        log.info("关单消息发送成功，{}", orderMessage);
    }

    /**
     * 支付
     * @param addOrderRequest 订单信息
     * @param outTradeNo 订单编号
     * @param item 订单中第一个订单项信息
     * @param timeout 订单的超时时间，可为null，如果是null，那么就是默认30分钟
     */
    private String doPlaceOrder(AddOrderRequest addOrderRequest, String outTradeNo, ProductOrderItemDO item,
                                Long timeout) {
        PayInfoVO payInfoVO = new PayInfoVO(addOrderRequest, outTradeNo, item, timeout);
        String result = payStrategyContext.getPayStrategy(addOrderRequest.getPayType()).placeOrder(payInfoVO);

        // timeout为null是下单业务，而不是null时是二次支付业务
        String bizMessage = timeout == null ? "下单" : "二次支付";
        // 如果下单成功，result就有值，这个值其实是返回的form表单
        // 如果失败，result就没有值
        if (StrUtil.isBlank(result)) {
            log.error("{}失败。result：{}，payInfo：{}", bizMessage, result, payInfoVO);
            throw new BizException(50000, bizMessage + "失败");
        } else {
            log.info("{}成功。result：{}，payInfo：{}", bizMessage, result, payInfoVO);
            return result;
        }
    }

    /**
     * 按照结果、客户端类型和支付类型进行输出
     * @param form 输出结果
     * @param payType 支付类型
     * @param clientType 客户端类型
     */
    private void doOutputResultByType(String form, String payType, String clientType) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        HttpServletResponse response = requestAttributes.getResponse();

        //如果是支付宝网页支付，都是跳转网页，APP除外
        if (payType.equalsIgnoreCase(PayTypeEnum.ALIPAY.name())) {

            if (clientType.equalsIgnoreCase(ClientTypeEnum.H5.name())) {
                CommonUtils.wrapperResponseAsText(response, form);

            } else if(clientType.equalsIgnoreCase(ClientTypeEnum.APP.name())) {
                //APP SDK支付  TODO
            }

        } else if (payType.equalsIgnoreCase(PayTypeEnum.WECHAT.name())) {
            //微信支付 TODO
        }
    }

    /**
     * 根据订单编号查询订单信息
     *
     * @param outTradeNo 订单编号
     * @return
     */
    @Override
    public String getProductOrderStateByOutTradeNo(String outTradeNo) {
        QueryWrapper<ProductOrderDO> wrapper = new QueryWrapper<>();
        wrapper.eq("out_trade_no", outTradeNo);
        ProductOrderDO one = this.getOne(wrapper);
        if (one == null) {
            throw new BizException(BizCodeEnum.RETURN_DATA_IS_NONE);
        }
        return one.getState();
    }

    /**
     * 关单
     *
     * @param orderMessage 关单消息
     * @return
     */
    @Override
    public boolean doCloseOrder(OrderMessage orderMessage) {
        String outTradeNo = orderMessage.getOutTradeNo();
        ProductOrderDO productOrderDO = productOrderMapper
                .selectOne(new QueryWrapper<ProductOrderDO>()
                .eq("out_trade_no", outTradeNo));
        if (productOrderDO == null) {
            log.info("订单不存在，{}", orderMessage);
            return false;
        }

        if (productOrderDO.getState().equals(ProductOrderStateEnum.PAY.name())) {
            log.info("订单已成功支付，{}", orderMessage);
            return false;
        }

        if (productOrderDO.getState().equals(ProductOrderStateEnum.CANCEL.name())) {
            log.warn("订单已被取消，{}", orderMessage);
            return false;
        }

        if (productOrderDO.getState().equals(ProductOrderStateEnum.NEW.name())) {
            PayInfoVO payInfoVO = new PayInfoVO();
            payInfoVO.setOutTradeNo(outTradeNo);

            String result = payStrategyContext.getPayStrategy(productOrderDO.getPayType()).queryPaySuccess(payInfoVO);

            // 同样，查询成功时result有值，查询失败时，result没有值
            if (StrUtil.isNotBlank(result)) {
                log.info("订单的支付已经成功，造成这种情况可能是第三方支付回调没有成功。现要将订单的状态改为已支付，{}", orderMessage);
                productOrderDO.setState(ProductOrderStateEnum.PAY.name());
            } else {
                log.warn("订单未被支付，要将订单的状态改为已取消，{}", orderMessage);
                productOrderDO.setState(ProductOrderStateEnum.CANCEL.name());
            }
            int rows = productOrderMapper.updateById(productOrderDO);
            if (rows == 1) {
                return false;
            } else {
                // TODO 这里可以设置超时次数，以免无限制的投递
                log.info("关单时修改订单状态失败，需要重新投递消息，{}", orderMessage);
                return true;
            }
        }
        return true;
    }

    /**
     * 支付宝回调接口
     *
     * @return
     */
    @Override
    public String doCallback() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        HttpServletRequest request = requestAttributes.getRequest();

        Map<String, String> paramsMap = CommonUtils.convertRequestParamsToMap(request);

        log.info("支付宝的回调参数为：{}", paramsMap);

        String outTradeNo = paramsMap.get("out_trade_no");

        try {
            // 调用SDK验证签名
            boolean success = AlipaySignature.rsaCheckV1(paramsMap, AlipayConfig.ALIPAY_PUBLIC_KEY,
                    AlipayConfig.CHARSET,
                    AlipayConfig.SIGN_TYPE);

            if (success) {
                // 验签成功
                log.info("支付宝回调时验签成功，{}", outTradeNo);

                String tradeStatus = paramsMap.get("trade_status");

                // 能退款的产品并在能退款的时间内成功时返回TRADE_SUCCESS，不能退款的产品或过了退款时间成功时会返回TRADE_FINISHED
                if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {
                    // 将订单更新为已支付
                    updateOrderStatusSuccess(outTradeNo);
                }

                // 因为成功回调不意味着该笔支付必定成功，也会有支付失败的时候，所以只要验签过了，就要返回success
                // 否则支付宝那边会重试
                return "success";

            } else {
                // 验签失败
                log.error("支付宝回调时验签失败，{}", outTradeNo);
            }
        } catch (Exception e) {
            log.error("支付宝回调时验签失败，{}，原因是：{}", outTradeNo, e.getMessage(), e);
        }

        return "failure";
    }

    /**
     * 将订单状态置为已支付
     * @param outTradeNo 订单编号
     */
    private void updateOrderStatusSuccess(String outTradeNo) {
        ProductOrderDO orderDO = this.getOne(new QueryWrapper<ProductOrderDO>().eq("out_trade_no", outTradeNo));
        if (orderDO == null) {
            throw new BizException(50000, "订单信息不存在");
        }
        if (orderDO.getState().equals(ProductOrderStateEnum.PAY.name())) {
            log.info("该订单的状态已经是已支付，无需修改，{}", outTradeNo);
            return;
        }
        orderDO.setState(ProductOrderStateEnum.PAY.name());
        if (!this.updateById(orderDO)) {
            throw new BizException(50000, "将订单置为已支付时发生异常");
        }
    }

    /**
     * 分页条件查询订单信息
     *
     * @param page  当前页
     * @param size  每页记录数
     * @param state 订单状态
     * @return
     */
    @Override
    public Page<ProductOrderVO> pageOrder(int page, int size, String state) {
        LoginUser loginUser = UserAuthorization.getUserContext();

        QueryWrapper<ProductOrderDO> wrapper = new QueryWrapper<>();
        wrapper.eq(StrUtil.isNotBlank(state), "state", state)
                .eq("user_id", loginUser.getId());

        Page<ProductOrderDO> pageInfo = new Page<>(page, size);
        this.page(pageInfo, wrapper);

        if (CollectionUtil.isEmpty(pageInfo.getRecords())) {
            return new Page<>();
        }

        List<ProductOrderVO> list = pageInfo.getRecords().stream().map(item -> {
            ProductOrderVO vo = new ProductOrderVO(item);

            List<ProductOrderItemDO> items = productOrderItemMapper
                    .selectList(new QueryWrapper<ProductOrderItemDO>().eq("out_trade_no", vo.getOutTradeNo()));

            List<ProductOrderItemVO> vos = items.stream()
                    .map(ProductOrderItemVO::new).collect(Collectors.toList());
            vo.setProductOrderItems(vos);
            return vo;
        }).collect(Collectors.toList());


        Page<ProductOrderVO> pageVO = new Page<>();
        BeanUtils.copyProperties(pageInfo, pageVO);
        pageVO.setRecords(list);
        return pageVO;
    }

    /**
     * 重新支付订单
     *
     * @param repayOrderRequest 订单信息
     */
    @Override
    public void repay(RepayOrderRequest repayOrderRequest) {
        // 1.获取当前的登录用户
        LoginUser loginUser = UserAuthorization.getUserContext();

        // 2.获取订单编号
        String outTradeNo = repayOrderRequest.getOutTradeNo();

        // 3.获取原订单信息
        ProductOrderDO productOrderDO = this.getProductOrder(outTradeNo, loginUser);

        // 4.获取原订单项信息
        List<ProductOrderItemDO> itemList = this.getProductOrderItems(outTradeNo);

        // 5.如果二次支付的订单信息的clientType和原订单信息不同，则修改原订单信息
        this.updateProductOrderIfModified(productOrderDO, repayOrderRequest);

        // 6.构建本次的订单信息
        AddOrderRequest addOrderRequest = this.buildAddOrderRequest(productOrderDO, repayOrderRequest);

        // 7.计算本次订单的超时支付时间
        Long timeout = this.calculateTimeout(productOrderDO);

        // 8.支付
        String form = this.doPlaceOrder(addOrderRequest, outTradeNo, itemList.get(0), timeout);

        // 9.按照结果、客户端类型和支付类型进行输出
        this.doOutputResultByType(form, repayOrderRequest.getPayType(), repayOrderRequest.getClientType());
    }

    /**
     * 根据订单编号查询订单信息
     * @param outTradeNo 订单编号
     * @param loginUser 当前用户信息
     * @return
     */
    private ProductOrderDO getProductOrder(String outTradeNo, LoginUser loginUser) {
        QueryWrapper<ProductOrderDO> wrapper = new QueryWrapper<>();
        wrapper.eq("out_trade_no", outTradeNo)
                .eq("user_id", loginUser.getId());
        ProductOrderDO dbOrder = this.getOne(wrapper);
        if (dbOrder == null) {
            throw new BizException(50000, "对应的订单信息不存在");
        }
        if (!dbOrder.getState().equals(ProductOrderStateEnum.NEW.name())) {
            throw new BizException(50000, "订单状态不是NEW，不能发起二次支付");
        }
        return dbOrder;
    }

    /**
     * 根据订单编号查询订单项信息
     * @param outTradeNo 订单编号
     * @return
     */
    private List<ProductOrderItemDO> getProductOrderItems(String outTradeNo) {
        QueryWrapper<ProductOrderItemDO> wrapper = new QueryWrapper<>();
        wrapper.eq("out_trade_no", outTradeNo);
        List<ProductOrderItemDO> productOrderItemDOS = productOrderItemMapper.selectList(wrapper);
        if (CollectionUtil.isEmpty(productOrderItemDOS)) {
            throw new BizException(50000, "该订单没有订单项，信息非法");
        }
        return productOrderItemDOS;
    }

    /**
     * 如果二次支付的订单信息的clientType和原订单信息不同，则修改原订单信息
     * @param productOrderDO 原订单信息
     * @param repayOrderRequest 二次支付的订单信息
     */
    private void updateProductOrderIfModified(ProductOrderDO productOrderDO, RepayOrderRequest repayOrderRequest) {
        if (productOrderDO.getPayType().equals(repayOrderRequest.getPayType())) {
            return;
        }
        productOrderDO.setPayType(repayOrderRequest.getPayType());
        if (!this.updateById(productOrderDO)) {
            throw new BizException(50000, "修改订单信息失败");
        }
    }

    /**
     * 构建AddOrderRequest对象
     * @param productOrderDO 原订单信息
     * @param repayOrderRequest 二次支付的订单信息
     * @return
     */
    private AddOrderRequest buildAddOrderRequest(ProductOrderDO productOrderDO, RepayOrderRequest repayOrderRequest) {
        AddOrderRequest addOrderRequest = new AddOrderRequest();
        addOrderRequest.setActualAmount(productOrderDO.getPayAmount());
        addOrderRequest.setPayType(repayOrderRequest.getPayType());
        addOrderRequest.setClientType(repayOrderRequest.getClientType());
        return addOrderRequest;
    }

    /**
     * 计算订单还有多久能支付
     * @param productOrderDO 原订单信息
     * @return
     */
    private Long calculateTimeout(ProductOrderDO productOrderDO) {
        // 获取原订单的创建时间
        Date createTime = productOrderDO.getCreateTime();

        // 获取该订单的剩余时间 = 默认时间 - (当前时间 - 订单创建时间)
        long remain = OrderConstant.DEFAULT_ORDER_PAY_TIME_OUT - (System.currentTimeMillis() - createTime.getTime());

        // 需要留给用户进行支付的时间，在这里我们给它设置为70秒钟
        remain = remain - 70 * 1000;

        if (remain < 0) {
            throw new BizException(50000, "该订单已超过支付时间，不能进行支付");
        }

        // 返回的是毫秒
        return remain;
    }


    /**
     * 获取确认订单时的token（防止重复提交）
     *
     * @return
     */
    @Override
    public String getSubmitToken() {
        // 1.获取当前用户
        LoginUser loginUser = UserAuthorization.getUserContext();

        // 2.生成token
        String token = CommonUtils.getStringNumRandom(32);

        // 3.获取该token存放到redis中的键
        String redisKey = String.format(RedisKey.ORDER_SUBMIT_TOKEN, loginUser.getId());

        // 4.将token放到redis中去
        redisTemplate.opsForValue().set(redisKey, token, OrderConstant.DEFAULT_ORDER_PAY_TIME_OUT, TimeUnit.MILLISECONDS);

        return token;
    }

}
